package redis

import (
	"encoding/json"
	"fmt"
	"github.com/edgexfoundry/edgex-go/internal/app/models"
	"github.com/edgexfoundry/edgex-go/internal/app/pkg/controller/http"
	"github.com/edgexfoundry/go-mod-core-contracts/v2/common"
	"github.com/edgexfoundry/go-mod-core-contracts/v2/errors"
	"github.com/gomodule/redigo/redis"
	"strings"
	"time"

	pkgCommon "github.com/edgexfoundry/edgex-go/internal/pkg/common"
)

const (
	DeviceResourceCollection            = DeviceCollection + DBKeySeparator + common.Resource
	DeviceResourceNameCollection        = DeviceCollection + DBKeySeparator + common.ResourceName
	DeviceProfileNewCollection          = DeviceCollection + DBKeySeparator + common.Profile
	DeviceProfileResourceCollection     = DeviceCollection + DBKeySeparator + common.Profile + DBKeySeparator + common.Resource
	DeviceProfileResourceNameCollection = DeviceCollection + DBKeySeparator + common.Profile + DBKeySeparator + common.ResourceName
)

func checkDeviceResourceExist(conn redis.Conn, id string, code string) errors.EdgeX {
	exists, err := deviceResourceExists(conn, id, code)
	if err != nil {
		return errors.NewCommonEdgeX(errors.Kind(err), http.DataBaseQueryError, err)
	} else if !exists {
		return errors.NewCommonEdgeX(errors.KindInvalidId, fmt.Sprintf(http.DeviceResourceNotExist, code), nil)
	}
	return nil
}

// deviceResourceExists checks whether the device resource exists by code
func deviceResourceExists(conn redis.Conn, deviceId, code string) (bool, errors.EdgeX) {
	exists, err := objectIdExists(conn, CreateKey(DeviceResourceCollection, deviceId, code))
	if err != nil {
		return false, errors.NewCommonEdgeX(errors.KindDatabaseError, fmt.Sprintf(http.DeviceResourceNotExist, code), err)
	}
	return exists, nil
}

func deviceResourceGetByCode(conn redis.Conn, deviceId string, id string) (models.DeviceResource, errors.EdgeX) {
	var resource models.DeviceResource
	storedKey := CreateKey(DeviceResourceCollection, deviceId, id)
	edgeXerr := getObjectById(conn, storedKey, &resource)
	if edgeXerr != nil {
		return resource, errors.NewCommonEdgeXWrapper(edgeXerr)
	}
	return resource, nil
}

// deviceResourceAdd device resource add
func deviceResourceAdd(conn redis.Conn, deviceId string, drs ...models.DeviceResource) (string, errors.EdgeX) {

	deviceResourceKey := CreateKey(DeviceProfileResourceCollection, deviceId)

	deviceResourceName := CreateKey(DeviceProfileResourceNameCollection, deviceId)
	deviceCommandName := CreateKey(DeviceProfileCommandNameCollection, deviceId)

	resourceExistIds, resourceExistNames, commandExistNames, errRedis := handlerQueryResourceExistInfo(conn, deviceResourceKey, deviceResourceName, deviceCommandName)

	if errRedis != nil {
		return "", errors.NewCommonEdgeX(errors.KindDatabaseError, "query object names from database failed", errRedis)
	}

	resourceNames := make([]string, len(drs))
	_ = conn.Send(MULTI)
	for index, resource := range drs {
		// 原处理逻辑校验code不可为空，现调整为校验name不可为空
		name := resource.Name
		if name == "" {
			return "", errors.NewCommonEdgeX(errors.KindContractInvalid, http.DeviceNameNotNull, nil)
		}

		//调整Resource业务key为UUID，同时校验属性名称是否重复
		storedKey := CreateKey(DeviceResourceCollection, deviceId, resource.Id)
		if pkgCommon.IsStringsContain(resourceExistIds, storedKey) {
			return "", errors.NewCommonEdgeX(errors.KindDatabaseError, fmt.Sprintf(http.DeviceResourceIdAlreadyExist, resource.Id), nil)
		}
		nameKey := CreateKey(DeviceResourceNameCollection, deviceId, resource.Name)

		// 如果属性名称被占用
		if pkgCommon.IsStringsContain(resourceExistNames, nameKey) {
			return "", errors.NewCommonEdgeX(errors.KindDatabaseError, fmt.Sprintf(http.DeviceResourceNameAlreadyExist, resource.Name), nil)
		}

		// 如果功能名称被占用
		if pkgCommon.IsStringsContain(commandExistNames, CreateKey(DeviceCommandNameCollection, deviceId, resource.Name)) {
			return "", errors.NewCommonEdgeX(errors.KindDatabaseError, fmt.Sprintf(http.CommandNameAlreadyExist, resource.Name), nil)
		}

		ts := pkgCommon.MakeTimestamp()
		resource.Created = ts
		resource.Modified = ts

		r, err := json.Marshal(resource)
		if err != nil {
			return "", errors.NewCommonEdgeX(errors.KindContractInvalid, "query object ids from database failed", nil)
		}

		resourceNames[index] = resource.Name
		_ = conn.Send(SET, storedKey, r)
		_ = conn.Send(ZADD, deviceResourceKey, time.Now().UnixMilli(), storedKey)

		_ = conn.Send(SET, nameKey, r)
		_ = conn.Send(ZADD, deviceResourceName, time.Now().UnixMilli(), nameKey)
	}

	_ = conn.Send(EXEC)
	return strings.Join(resourceNames, ","), nil
}

func handlerQueryResourceExistInfo(conn redis.Conn, deviceResourceKey string, deviceResourceName string, deviceCommandName string) ([]string, []string, []string, error) {
	resourceExists, _ := objectIdExists(conn, deviceResourceKey)
	var resourceExistIds, resourceExistNames, commandExistNames []string

	// 查询属性id及属性name
	if resourceExists {
		ids, errRedis := redis.Strings(conn.Do(ZREVRANGE, deviceResourceKey, 0, -1))
		if errRedis != nil {
			return nil, nil, nil, errors.NewCommonEdgeX(errors.KindContractInvalid, "query object ids from database failed", errRedis)
		}
		resourceExistIds = ids

		names, errRedis := redis.Strings(conn.Do(ZREVRANGE, deviceResourceName, 0, -1))
		if errRedis != nil {
			return nil, nil, nil, errors.NewCommonEdgeX(errors.KindContractInvalid, "query object ids from database failed", errRedis)
		}
		resourceExistNames = names
	}

	//查询功能name
	commandExists, _ := objectIdExists(conn, deviceCommandName)
	if commandExists {
		names, errRedis := redis.Strings(conn.Do(ZREVRANGE, deviceCommandName, 0, -1))
		if errRedis != nil {
			return nil, nil, nil, errors.NewCommonEdgeX(errors.KindContractInvalid, "query object ids from database failed", errRedis)
		}
		commandExistNames = names
	}

	return resourceExistIds, resourceExistNames, commandExistNames, nil
}

// deviceResourceDelete device resource delete
func deviceResourceDelete(conn redis.Conn, deviceId, id string) (string, errors.EdgeX) {

	deviceResourceKey := CreateKey(DeviceProfileResourceCollection, deviceId)
	deviceResourceName := CreateKey(DeviceProfileResourceNameCollection, deviceId)

	exists, _ := objectIdExists(conn, deviceResourceKey)
	var memberIds []string
	if exists {
		ids, errRedis := redis.Strings(conn.Do(ZREVRANGE, deviceResourceKey, 0, -1))
		if errRedis != nil {
			return "", errors.NewCommonEdgeX(errors.KindDatabaseError, http.DataBaseQueryError, errRedis)
		}
		memberIds = ids
	}

	storedKey := CreateKey(DeviceResourceCollection, deviceId, id)

	if !pkgCommon.IsStringsContain(memberIds, storedKey) {
		return "", errors.NewCommonEdgeX(errors.KindEntityDoesNotExist, fmt.Sprintf(http.DeviceResourceNotExist, id), nil)
	}

	//同步删除name校验key，防止删除的属性名称无法再次录入
	var resource models.DeviceResource
	err := getObjectById(conn, storedKey, &resource)
	if err != nil {
		return "", errors.NewCommonEdgeXWrapper(err)
	}
	nameKey := CreateKey(DeviceResourceNameCollection, deviceId, resource.Name)

	values, _ := getValueByHash(conn, CreateKey(DeviceResourceResourceOperation, deviceId))

	if len(values) > 0 && pkgCommon.IsStringsContain(values, CreateKey(DeviceResourceCollection, deviceId, id)) {
		return "", errors.NewCommonEdgeX(errors.KindStatusConflict, fmt.Sprintf(http.DeviceResourceAlreadyInUse, id), nil)
	}

	_ = conn.Send(ZREM, deviceResourceKey, storedKey)
	_ = conn.Send(DEL, storedKey)

	_ = conn.Send(ZREM, deviceResourceName, nameKey)
	_ = conn.Send(DEL, nameKey)

	return id, nil
}

func deleteAllResource(conn redis.Conn, deviceId string) errors.EdgeX {
	deviceResourceKey := CreateKey(DeviceProfileResourceCollection, deviceId)

	fields, _ := fieldsByZSet(conn, deviceResourceKey)
	_ = conn.Send(DEL, fields)
	_ = conn.Send(DEL, deviceResourceKey)

	_ = deleteKeysPattern(conn, CreateKey(DeviceResourceCollection, deviceId, "*"))
	return nil
}

// deviceResourceUpdate device resource update
func deviceResourceUpdate(conn redis.Conn, deviceId string, dr models.DeviceResource) (string, errors.EdgeX) {

	deviceResourceKey := CreateKey(DeviceProfileResourceCollection, deviceId)
	deviceResourceName := CreateKey(DeviceProfileResourceNameCollection, deviceId)
	deviceCommandName := CreateKey(DeviceProfileCommandNameCollection, deviceId)

	resourceExistIds, resourceExistNames, commandExistNames, errRedis := handlerQueryResourceExistInfo(conn, deviceResourceKey, deviceResourceName, deviceCommandName)

	if errRedis != nil {
		return "", errors.NewCommonEdgeX(errors.KindDatabaseError, "query object names from database failed", errRedis)
	}

	storedKey := CreateKey(DeviceResourceCollection, deviceId, dr.Id)
	if !pkgCommon.IsStringsContain(resourceExistIds, storedKey) {
		return "", errors.NewCommonEdgeX(errors.KindDatabaseError, fmt.Sprintf(http.DeviceResourceNotExist, dr.Id), nil)
	}

	//判断属性名字不能重复，同时要排除自身
	origin, err := deviceResourceGetByCode(conn, deviceId, dr.Id)
	if err != nil {
		return "", errors.NewCommonEdgeX(errors.KindDatabaseError, fmt.Sprintf(http.DeviceResourceNotExist, dr.Id), nil)
	}

	nameKey := CreateKey(DeviceResourceNameCollection, deviceId, dr.Name)
	oldNameKey := CreateKey(DeviceResourceNameCollection, deviceId, origin.Name)

	// 如果属性名称被占用
	if pkgCommon.IsStringsContain(resourceExistNames, nameKey) && origin.Name != dr.Name {
		return "", errors.NewCommonEdgeX(errors.KindDatabaseError, fmt.Sprintf(http.DeviceResourceNameAlreadyExist, dr.Name), nil)
	}

	// 如果功能名称被占用
	if pkgCommon.IsStringsContain(commandExistNames, CreateKey(DeviceCommandNameCollection, deviceId, dr.Name)) {
		return "", errors.NewCommonEdgeX(errors.KindDatabaseError, fmt.Sprintf(http.CommandNameAlreadyExist, dr.Name), nil)
	}

	origin.Name = dr.Name
	origin.IsHidden = dr.IsHidden
	origin.Description = dr.Description
	origin.Properties = dr.Properties

	ts := pkgCommon.MakeTimestamp()
	origin.Modified = ts

	r, errJson := json.Marshal(origin)
	if errJson != nil {
		return "", errors.NewCommonEdgeX(errors.KindContractInvalid, http.ParamsError, errJson)
	}

	_ = conn.Send(SET, storedKey, r)
	//处理name校验key，删掉原key，增加新key
	_ = conn.Send(ZREM, deviceResourceName, oldNameKey)
	_ = conn.Send(DEL, oldNameKey)

	_ = conn.Send(SET, nameKey, r)
	_ = conn.Send(ZADD, deviceResourceName, time.Now().UnixMilli(), nameKey)

	return dr.Code, nil
}

func deviceResourcesGetByDeviceId(conn redis.Conn, deviceId string, offset, limit int) (resources []models.DeviceResource, e errors.EdgeX) {

	deviceResourceKey := CreateKey(DeviceProfileResourceCollection, deviceId)

	objects, err := getObjectsByRevRange(conn, deviceResourceKey, offset, limit)
	if errors.Kind(err) == errors.KindEntityDoesNotExist {
		return // Empty Readings in an Event is not an error
	} else if err != nil {
		return resources, errors.NewCommonEdgeXWrapper(err)
	}

	resources = make([]models.DeviceResource, len(objects))
	for index, obj := range objects {
		var resource models.DeviceResource
		err := json.Unmarshal(obj, &resource)
		if err != nil {
			continue
		}

		resources[index] = resource
	}
	return
}
